package org.zhaya.sdk.swamill;

import com.google.common.base.Stopwatch;
import io.logz.sawmill.Processor;
import io.logz.sawmill.ProcessorFactoryRegistry;
import io.logz.sawmill.annotations.ProcessorProvider;
import io.logz.sawmill.exceptions.SawmillException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import org.zhaya.sdk.DodpProcessor;

import javax.inject.Inject;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.stream.Stream;

import static java.util.concurrent.TimeUnit.MILLISECONDS;

@Data
@Slf4j
public class CustomProcessorFactoriesLoader {
    private Map<Class<?>, Object> dependenciesToInject;
    private Reflections reflections;

    public CustomProcessorFactoriesLoader(ProcessorFactoryRegistry processorFactoryRegistry, Map<Class<?>, Object> dependenciesToInjectMap, String prefix) {
        this.dependenciesToInject = dependenciesToInjectMap;
        this.reflections = new Reflections(prefix);
        this.loadAnnotatedProcessors(processorFactoryRegistry);
    }

    public CustomProcessorFactoriesLoader(ProcessorFactoryRegistry processorFactoryRegistry, Map<Class<?>, Object> dependenciesToInjectMap) {
        this.dependenciesToInject = dependenciesToInjectMap;
        this.loadSpiProcessors(processorFactoryRegistry);
    }

    private void loadAnnotatedProcessors(ProcessorFactoryRegistry processorFactoryRegistry) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        long timeElapsed = 0;

        int processorsLoaded = 0;
        Set<Class<?>> processors = reflections.getTypesAnnotatedWith(ProcessorProvider.class);
        for (Class<?> processor : processors) {
            try {
                ProcessorProvider processorProvider = processor.getAnnotation(ProcessorProvider.class);
                String typeName = processorProvider.type();
                Optional<Processor.Factory> factoryOptional = getFactory(processorProvider);
                if (factoryOptional.isPresent()) {
                    processorFactoryRegistry.register(typeName, factoryOptional.get());
                    log.debug("{} processor factory loaded successfully, took {}ms", typeName, stopwatch.elapsed(MILLISECONDS) - timeElapsed);
                    processorsLoaded++;
                }
            } catch (Exception e) {
                throw new SawmillException(String.format("failed to load processor %s", processor.getName()), e);
            } finally {
                timeElapsed = stopwatch.elapsed(MILLISECONDS);
            }
        }
        log.debug("{} processor factories loaded, took {}ms", processorsLoaded, stopwatch.elapsed(MILLISECONDS));
    }

    private void loadSpiProcessors(ProcessorFactoryRegistry processorFactoryRegistry) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        long timeElapsed = 0;

        int processorsLoaded = 0;
        ServiceLoader<DodpProcessor> processors = ServiceLoader.load(DodpProcessor.class);
        for (DodpProcessor processor : processors) {
            try {
                ProcessorProvider processorProvider = processor.getClass().getAnnotation(ProcessorProvider.class);
                String typeName = processorProvider.type();
                Optional<Processor.Factory> factoryOptional = getFactory(processorProvider);
                if (factoryOptional.isPresent()) {
                    processorFactoryRegistry.register(typeName, factoryOptional.get());
                    log.debug("{} processor factory loaded successfully, took {}ms", typeName, stopwatch.elapsed(MILLISECONDS) - timeElapsed);
                    processorsLoaded++;
                }
            } catch (Exception e) {
                throw new SawmillException(String.format("failed to load processor %s", processor.getClass().getName()), e);
            } finally {
                timeElapsed = stopwatch.elapsed(MILLISECONDS);
            }
        }
        log.debug("{} processor factories loaded, took {}ms", processorsLoaded, stopwatch.elapsed(MILLISECONDS));
    }

    private Optional<Processor.Factory> getFactory(ProcessorProvider processorProvider) throws InstantiationException, IllegalAccessException, java.lang.reflect.InvocationTargetException, NoSuchMethodException {
        Class<? extends Processor.Factory> factoryType = processorProvider.factory();
        Optional<? extends Constructor<?>> injectConstructor = Stream.of(factoryType.getConstructors())
                .filter(constructor -> constructor.isAnnotationPresent(Inject.class)).findFirst();
        if (injectConstructor.isPresent()) {
            Class<?>[] servicesToInject = injectConstructor.get().getParameterTypes();
            if (Arrays.stream(servicesToInject).allMatch(serviceType -> checkDependencyPresent(processorProvider, serviceType))) {
                Object[] servicesInstance = Stream.of(servicesToInject).map(dependenciesToInject::get).toArray();
                return Optional.of(factoryType.getConstructor(servicesToInject).newInstance(servicesInstance));
            } else {
                return Optional.empty();
            }

        } else {
            return Optional.of(factoryType.getConstructor().newInstance());
        }
    }


    private boolean checkDependencyPresent(ProcessorProvider processorProvider, Class<?> serviceType) {
        if (!dependenciesToInject.containsKey(serviceType)) {
            log.warn(String.format(
                    "Could not instantiate %s processor factory, %s dependency missing",
                    processorProvider.factory().getName(),
                    serviceType.getSimpleName()
            ));
            return false;
        }
        return true;
    }
}
